home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Tricks of the Mac Game Programming Gurus
/
TricksOfTheMacGameProgrammingGurus.iso
/
Book Chapters
/
05 - User Interaction
/
Dungeon 2
/
Dungeon2.c
< prev
next >
Wrap
Text File
|
1995-04-01
|
21KB
|
785 lines
/*Dungeon2 – prototype game example for the Mac Game book*/
/*By Ingemar Ragnemalm 1995*/
/**/
/*This game is somewhat similar to MemoryGame, in that it uses a grid, represented*/
/*by an array. The game is a typical (though extremely simplified) dungeon-digging game,*/
/*where the objective is to collect treasures and fight monsters.*/
/*Representation:*/
/*The array tileArr holds nearly all information we need. telling what is in each space in the grid.*/
/*Monsters and treasures are only represented this way. In a real game, you may wish to*/
/*keep a list of all monster and treasure positions, to avoid scanning for them and to keep more*/
/*information about each.*/
/*The game also keeps an array that tells what spaces are known to the player (tilesKnown), and*/
/*the player position (playerPosition), so we don't have to scan for it all the time.*/
/*The combat system is extremely simple. If you try moving to an enemy, you have 60% chance to*/
/*hit it and kill it. If an enemy tries to move to you, it has 50% chance to hit, reducing your hit points*/
/*by one.*/
/**/
/*The program ends when we select "Quit".*/
/*This version adds the following features:*/
/*- Keyboard control as well as mouse control*/
/*- Event processing, menus etc*/
#include <Sound.h>
/*Size of the array*/
#define kArraySizeH 15
#define kArraySizeV 12
/*Size of the tiles*/
#define kTileSizeH 32
#define kTileSizeV 32
/*Menu ids*/
#define appleID 127
#define fileID 128
/* A macro for taking the abs of a value */
#define abs(x) (x>0?x:-x)
/* All the possible states of a tile (space in the dungeon) */
typedef enum {empty, wall, player, enemy, tempEnemy, gold, exitPos} TileState;
/* The window pointer */
WindowPtr myWindow;
/* Arrays describing the dungeon */
/* What tiles have we seen? */
Boolean tileKnown[kArraySizeH][kArraySizeV];
/* What does each tile contain? */
TileState tileArray[kArraySizeH][kArraySizeV];
/*Variables describing the player:*/
Point playerPosition;
short playerHitPoints;
/* A boolean telling if we should quit yet or not */
Boolean gDone = false;
/*Pictures*/
PicHandle floorTile;
PicHandle playerTile;
PicHandle enemyTile;
PicHandle goldTile;
PicHandle wallTile;
PicHandle exitTile;
/*All 8 directions as vectors*/
Point directionTable[8] = { { 0, 1 },
{-1, 1 },
{-1, 0 },
{-1,-1 },
{ 0,-1 },
{ 1,-1 },
{ 1, 0 },
{ 1, 1 }};
/* A function that generates a value in the interval 0..range-1 */
static short Rand(short range)
{
return (Random () & 0x7fff) % range;
}; /*Rand*/
/* Draw a tile */
static void DrawTile(short h, short v)
{
Rect tileRectangle;
SetRect(&tileRectangle, h * kTileSizeH, v * kTileSizeV, (h + 1) * kTileSizeH, (v + 1) * kTileSizeV);
if ( tileKnown[h][v] )
switch ( tileArray[h][v] )
{
case empty:
DrawPicture(floorTile, &tileRectangle); break;
case wall:
DrawPicture(wallTile, &tileRectangle); break;
case player:
DrawPicture(playerTile, &tileRectangle); break;
case enemy:
case tempEnemy:
DrawPicture(enemyTile, &tileRectangle); break;
case gold:
DrawPicture(goldTile, &tileRectangle); break;
case exitPos:
DrawPicture(exitTile, &tileRectangle); break;
default:
PaintRect(&tileRectangle);
}
else
PaintRect(&tileRectangle);
} /*DrawTile*/
/* Set all tiles around the player to be known, and draw them if they were not known before. */
static void ShowAroundPlayer()
{
short h, v;
for ( h = playerPosition.h - 1 ; h <= playerPosition.h + 1 ; h++)
for ( v = playerPosition.v - 1 ; v <= playerPosition.v + 1 ; v++)
if ( ! tileKnown[h][v] )
{
tileKnown[h][v] = true;
DrawTile(h, v);
}
} /*ShowAroundPlayer*/
/* The level generation routine */
static void CreateLevel()
{
Rect roomRect[10];
short room;
short height, width;
short h, v, h1, v1, h2, v2;
short numRooms;
short numTreasures, numMonsters;
short i;
/*For each tile position, we set the initial state.*/
/**/
/*Here we generate the dungeon randomly, with a rather simple algorithm.*/
/*Most real games use pre-designed levels, preferrably stored in resources.*/
/*Dungeon generation algorithm:*/
/*We select a number of rooms to be placed, 3-10*/
/*Each room is randomly assigned a size, and then a position.*/
/*In each room we may put monsters and/or treasures.*/
/*From each room except the last, we make a path from the room to the next.*/
/*This guarantees that all rooms are connected.*/
/*First fill the entire dungeon with walls!*/
for ( h = 0 ; h < kArraySizeH ; h++)
for ( v = 0 ; v < kArraySizeV ; v++)
tileArray[h][v] = wall;
numRooms = 3 + Rand(4); /*3 to 6 rooms*/
/*Create each room*/
for ( room = 0 ; room <= numRooms - 1 ; room++)
{
height = 1 + Rand(5 - numRooms / 2);
width = 1 + Rand(5 - numRooms / 2);
roomRect[room].top = 1 + Rand(kArraySizeV - height - 2);
roomRect[room].bottom = roomRect[room].top + height;
roomRect[room].left = 1 + Rand(kArraySizeH - width - 2);
roomRect[room].right = roomRect[room].left + width;
for ( h = roomRect[room].left ; h <= roomRect[room].right ; h++)
for ( v = roomRect[room].top ; v <= roomRect[room].bottom ; v++)
tileArray[h][v] = empty;
};
/*Make paths between all rooms*/
for ( room = 0 ; room <= numRooms - 2 ; room++)
{
/*We make a path from h1, h2, to h2, v2*/
h1 = roomRect[room].left + Rand(roomRect[room].right - roomRect[room].left + 1);
v1 = roomRect[room].top + Rand(roomRect[room].bottom - roomRect[room].top + 1);
h2 = roomRect[room + 1].left + Rand(roomRect[room + 1].right - roomRect[room + 1].left + 1);
v2 = roomRect[room + 1].top + Rand(roomRect[room + 1].bottom - roomRect[room + 1].top + 1);
/*First move along the h axis*/
if ( h1 < h2 )
for ( h = h1 ; h <= h2 ; h++)
tileArray[h][v1] = empty;
else
for ( h = h1 ; h >= h2 ; h--)
tileArray[h][v1] = empty;
/*And then along the v axis*/
if ( v1 < v2 )
for ( v = v1 ; v <= v2 ; v++)
tileArray[h2][v] = empty;
else
for ( v = v1 ; v >= v2 ; v--)
tileArray[h2][v] = empty;
};
/*Now populate the rooms!*/
for ( room = 1 ; room <= numRooms - 1 ; room++)
{
numTreasures = Rand(3); /*0 to 2 treasures*/
for ( i = 1 ; i <= numTreasures ; i++)
{
h = roomRect[room].left + Rand(roomRect[room].right - roomRect[room].left + 1);
v = roomRect[room].top + Rand(roomRect[room].bottom - roomRect[room].top + 1);
tileArray[h][v] = gold;
};
numMonsters = Rand(2); /*0 to 1 monsters*/
for ( i = 1 ; i <= numMonsters; i++)
{
h = roomRect[room].left + Rand(roomRect[room].right - roomRect[room].left + 1);
v = roomRect[room].top + Rand(roomRect[room].bottom - roomRect[room].top + 1);
tileArray[h][v] = enemy;
};
};
/*Finally, place the player in the first room and the exit in the last. Also check that the exit is*/
/*not the same position as the player! (It can happen, since we don't mak sure that rooms don't*/
/*overlap!)*/
/*Player position:*/
playerPosition.h = roomRect[0].left + Rand(roomRect[0].right - roomRect[0].left + 1);
playerPosition.v = roomRect[0].top + Rand(roomRect[0].bottom - roomRect[0].top + 1);
tileArray[playerPosition.h][playerPosition.v] = player;
/*Exit position:*/
h = roomRect[numRooms - 1].left + Rand(roomRect[numRooms - 1].right - roomRect[numRooms - 1].left + 1);
v = roomRect[numRooms - 1].top + Rand(roomRect[numRooms - 1].bottom - roomRect[numRooms - 1].top + 1);
/*But please don't overwrite the player with the exit!*/
if ( h == playerPosition.h )
if ( v == playerPosition.v )
{
/*Try another room:*/
h = roomRect[numRooms - 2].left + Rand(roomRect[numRooms - 2].right - roomRect[numRooms - 2].left + 1);
v = roomRect[numRooms - 2].top + Rand(roomRect[numRooms - 2].bottom - roomRect[numRooms - 2].top + 1);
/*Still failure? Darn. Just take a space next to the player.*/
if ( h == playerPosition.h )
if ( v == playerPosition.v )
if ( h < kArraySizeH )
h = h + 1;
else
h = h - 1;
};
/*OK, that's enough. Set the exit!*/
tileArray[h][v] = exitPos;
/*All tiles are unknown when we start*/
for ( h = 0 ; h < kArraySizeH ; h++)
for ( v = 0 ; v < kArraySizeV ; v++)
tileKnown[h][v] = false;
/*…except the ones around the player*/
/*Make the spaces around the player known*/
ShowAroundPlayer();
/*Draw all tiles!*/
for ( h = 0 ; h < kArraySizeH ; h++)
for ( v = 0 ; v < kArraySizeV ; v++)
DrawTile(h, v);
} /*CreateLevel*/
/* Move an enemy */
static void MoveEnemy(short h, short v)
{
short dist;
short newh, newv;
short dir;
/*1: decide if we are close to enough to the player to "hear" the player*/
dist = abs(h - playerPosition.h) + abs(v - playerPosition.v); /*City Block distance*/
/*2: Make a suggested destination, newh, newv*/
if ( dist < Rand(15) ) /*Move towards the player*/
{
if ( h < playerPosition.h )
newh = h + 1;
else if ( h > playerPosition.h )
newh = h - 1;
else
newh = h;
if ( v < playerPosition.v )
newv = v + 1;
else if ( v > playerPosition.v )
newv = v - 1;
else
newv = v;
}
else /*Move randomly*/
{
dir = Rand(8);
newh = h + directionTable[dir].h;
newv = v + directionTable[dir].v;
};
/*3: Check what is in the destination and take appropriate action (move, fight)*/
switch ( tileArray[newh][newv] )
{
case empty:
tileArray[newh][newv] = tempEnemy; /*We can't use "enemy", since then we might process it again in the same move!*/
tileArray[h][v] = empty;
DrawTile(newh, newv);
DrawTile(h, v);
break;
case player:
if ( Rand(10) > 5 ) /*Does it hit?*/
{ /*Enemy hits player!*/
playerHitPoints--; /*Reduce player hit points*/
if ( playerHitPoints > 0 )
{ /*Player still lives!*/
if ( noErr != SndPlay(nil, (SndListHandle)GetNamedResource('snd ', "\pPlayer hit"), false) )
;
}
else
{ /*Player died!*/
if ( noErr != SndPlay(nil, (SndListHandle)GetNamedResource('snd ', "\pPlayer died"), false) )
;
};
}
else
{ /*Miss!*/
if ( noErr != SndPlay(nil, (SndListHandle)GetNamedResource('snd ', "\pPlayer miss"), false) )
;
};
break;
case wall:
case enemy:
case gold:
case exitPos:
case tempEnemy:
; /*Don't move to any of these!*/
}; /*case*/
} /*MoveEnemy*/
/* Initialize - create window, load graphics */
static void InitDungeon()
{
Rect windowRectangle;
/*Set up the window*/
SetRect(&windowRectangle, 50, 50, 50 + kArraySizeH * kTileSizeH, 50 + kArraySizeV * kTileSizeV);
myWindow = NewCWindow(nil, &windowRectangle, "\pDungeon 2", true, documentProc, (WindowPtr)-1L, false, 0);
SetPort(myWindow);
qd.randSeed = TickCount (); /*Seed the random number generator*/
/*Load all pictures*/
floorTile = GetPicture(128); /*PICT resource #128.*/
playerTile = GetPicture(129); /*PICT resource #129.*/
enemyTile = GetPicture(130); /*PICT resource #130.*/
goldTile = GetPicture(131); /*PICT resource #131.*/
wallTile = GetPicture(132); /*PICT resource #132.*/
exitTile = GetPicture(133); /*PICT resource #133.*/
} /*InitDungeon*/
/* Set up for a new game */
static void NewGame()
{
/* Fill in the tileArr array */
CreateLevel();
/* Start with a healthy player */
playerHitPoints = 5;
} /*NewGame*/
/* ValidMove checks if a tile clickedTile is inside the array bounds *and* near the player */
static Boolean ValidMove(Point clickedTile)
{
/* Valid tile?*/
if ( clickedTile.h >= 0 )
if ( clickedTile.v >= 0 )
if ( clickedTile.h < kArraySizeH )
if ( clickedTile.v < kArraySizeV )
/* OK, we are inside the game area, clicking in some space! Is it next to the player?*/
if ( clickedTile.h >= playerPosition.h - 1 )
if ( clickedTile.h <= playerPosition.h + 1 )
if ( clickedTile.v >= playerPosition.v - 1 )
if ( clickedTile.v <= playerPosition.v + 1 )
return true;
return false;
} /*ValidMove*/
/* Try to move the player to the position where we clicked. */
static void MovePlayer(Point clickedTile)
{
short h, v;
/* Valid move?*/
if (ValidMove(clickedTile))
/* Yes! What is there? */
{
switch ( tileArray[clickedTile.h][clickedTile.v] )
{
case empty:
tileArray[playerPosition.h][playerPosition.v] = empty;
tileArray[clickedTile.h][clickedTile.v] = player;
DrawTile(playerPosition.h, playerPosition.v);
DrawTile(clickedTile.h, clickedTile.v);
playerPosition = clickedTile;
break;
case gold:
tileArray[playerPosition.h][playerPosition.v] = empty;
tileArray[clickedTile.h][clickedTile.v] = player;
DrawTile(playerPosition.h, playerPosition.v);
DrawTile(clickedTile.h, clickedTile.v);
playerPosition = clickedTile;
/* We could add score here */
if ( noErr != SndPlay(nil, (SndListHandle)GetNamedResource('snd ', "\pMoney"), false) )
;
break;
case wall:
SysBeep(1);
break;
case enemy:
if ( Rand(10) > 4 )
{ /*Hit! Play a "monster died" sound*/
if ( noErr != SndPlay(nil, (SndListHandle)GetNamedResource('snd ', "\pEnemy died"), false) )
;
/* Walk to the space where the monster was.*/
tileArray[playerPosition.h][playerPosition.v] = empty;
tileArray[clickedTile.h][clickedTile.v] = player;
DrawTile(playerPosition.h, playerPosition.v);
DrawTile(clickedTile.h, clickedTile.v);
playerPosition = clickedTile;
}
else
{ /*Miss! Play the "miss" sound. */
if ( noErr != SndPlay(nil, (SndListHandle)GetNamedResource('snd ', "\pEnemy miss"), false) )
;
};
break;
/*attack!*/
case exitPos:
tileArray[playerPosition.h][playerPosition.v] = empty;
tileArray[clickedTile.h][clickedTile.v] = player;
DrawTile(playerPosition.h, playerPosition.v);
DrawTile(clickedTile.h, clickedTile.v);
playerPosition = clickedTile;
if ( noErr != SndPlay(nil, (SndListHandle)GetNamedResource('snd ', "\pNext level"), false) )
;
CreateLevel(); /* Don't quit - make a new level instead */
break;
}; /*case*/
};
ShowAroundPlayer();
/* Monsters are allowed to move now!*/
/* Move all enemies!*/
for ( h = 0 ; h < kArraySizeH ; h++)
for ( v = 0 ; v < kArraySizeV ; v++)
if ( tileArray[h][v] == enemy )
MoveEnemy(h, v);
/* After moving, replace tempEnemy by enemy.*/
for ( h = 0 ; h < kArraySizeH ; h++)
for ( v = 0 ; v < kArraySizeV ; v++)
if ( tileArray[h][v] == tempEnemy )
tileArray[h][v] = enemy;
} /*MovePlayer*/
/* Handle mouse downs */
static void DoMouse(Point clickPoint, long mods)
{
Point clickedTile;
/* If the hero is dead, we can't move! */
if ( playerHitPoints < 1 )
{
SysBeep(1);
return;
};
/*Convert clickPoint to local coordinates*/
GlobalToLocal(&clickPoint);
/*Convert it to coordinates in the arrays.*/
clickedTile.h = clickPoint.h / kTileSizeH;
clickedTile.v = clickPoint.v / kTileSizeV;
MovePlayer(clickedTile);
} /*DoMouse*/
/* Empty stub for a background task. */
static void DoBackground()
{
} /*DoBackground*/
/* Add two points - simplifies the keydown handler */
static Point AddPoints(Point p1,Point p2)
{
Point dest;
dest.h = p1.h + p2.h;
dest.v = p1.v + p2.v;
return dest;
}; /*AddPoints*/
/* Handle key downs */
static void DoKey(char theKey, long mods)
{
/* If the hero is dead, we can't move! */
if ( playerHitPoints < 1 )
{
SysBeep(1);
return;
};
/* For now, we use a hard-coded key mapping. */
switch ( theKey )
{
case 'd':
case '6':
MovePlayer(AddPoints(playerPosition, directionTable[0]));break;
case 'e':
case '9':
MovePlayer(AddPoints(playerPosition, directionTable[1]));break;
case 'w':
case '8':
MovePlayer(AddPoints(playerPosition, directionTable[2]));break;
case 'q':
case '7':
MovePlayer(AddPoints(playerPosition, directionTable[3]));break;
case 'a':
case '4':
MovePlayer(AddPoints(playerPosition, directionTable[4]));break;
case 'z':
case '1':
MovePlayer(AddPoints(playerPosition, directionTable[5]));break;
case 'x':
case '2':
MovePlayer(AddPoints(playerPosition, directionTable[6]));break;
case 'c':
case '3':
MovePlayer(AddPoints(playerPosition, directionTable[7]));break;
default:
SysBeep(1);
}; /*case*/
}; /*DoKey*/
/* Handle selections in the File menu */
static void DoFileMenu(short item)
{
switch ( item )
{
case 1:
NewGame();break;
case 3:
gDone = true;break;
}; /*case*/
}; /*DoFileMenu*/
/* The "About" item was selected */
static void DoAbout()
{
short ignore;
ignore = Alert(128, nil);
}; /*DoAbout*/
/* Handle update events */
static void DoUpdate()
{
short h, v;
BeginUpdate(myWindow);
/*Draw all tiles!*/
for ( h = 0 ; h < kArraySizeH ; h++)
for ( v = 0 ; v < kArraySizeV ; v++)
DrawTile(h, v);
EndUpdate(myWindow);
}; /*DoUpdate*/
/* Handle menu selections */
static void DoMenuSelection(long mSelect)
{
short menuID;
short menuItem;
GrafPtr savePort;
Str255 name;
short ignore;
menuID = HiWord(mSelect);
menuItem = LoWord(mSelect);
switch ( menuID )
{
case appleID:
if ( menuItem == 1 )
DoAbout();
else
{
GetPort(&savePort);
GetItem(GetMHandle(appleID), menuItem, name);
ignore = OpenDeskAcc(name);
SetPort(savePort);
};
break;
case fileID:
DoFileMenu(menuItem);break;
default:
;}; /*case*/
HiliteMenu(0);
}; /*DoMenuSelection*/
/* Set up menus */
static void SetupMenus()
{
MenuHandle appleMenu, fileMenu;
Str255 tempStr;
tempStr[0]=1; tempStr[1] = (char)20;
appleMenu = NewMenu(appleID, tempStr); /*Apple menu symbol*/
InsertMenu(appleMenu, 0);
AppendMenu(appleMenu, "\pAbout Dungeon…;(-");
AppendResMenu(appleMenu, 'DRVR');
fileMenu = GetMenu(128);
InsertMenu(fileMenu, 0);
DrawMenuBar ();
}; /*SetupMenus*/
/* Main event loop */
static void MainLoop(void)
{
#define kSleep 5 /*Real programs may modify the sleep time depending on whether or not they are in the front*/
EventRecord theEvent;
char theKey;
long whatSelection;
short whichPart;
WindowPtr whichWindow;
Point diskInitPt = {40,40};
/*Get the next event with WaitNextEvent.*/
if ( WaitNextEvent(everyEvent, &theEvent, kSleep, nil) )
switch ( theEvent.what )
{
case mouseDown:
/*We must find out what kind of mouse down this was.*/
whichPart = FindWindow(theEvent.where, &whichWindow);
switch ( whichPart )
{
case inMenuBar:
/*Click in menu bar. Let the system call MenuSelect track it.*/
whatSelection = MenuSelect(theEvent.where);
DoMenuSelection(whatSelection); /*Our own routine for handling menu selections*/
break;
case inSysWindow:
/*Click in some window that isn't ours*/
SystemClick(&theEvent, whichWindow);
break;
case inGoAway:
/*Click in close box of window. For "desk accessory"-style games, that is a good quit signal*/
if ( (TrackGoAway(whichWindow, theEvent.where)) )
gDone = true;
break;
case inDrag:
/*Drag a window*/
if ( (whichWindow != FrontWindow ()) && ((theEvent.modifiers & cmdKey) == 0) )
SelectWindow(whichWindow);
DragWindow(whichWindow, theEvent.where, &qd.screenBits.bounds);
break;
case inGrow:
; /*Ignored - we don't resize*/
break;
case inContent:
/*Click in the window.*/
if ( (whichWindow != FrontWindow ()) )
SelectWindow(whichWindow);
else
DoMouse(theEvent.where, theEvent.modifiers); /*Go to application-specific mouse down handling*/
}; /*case whichPart*/
break; /*mouseDown*/
case keyDown:
case autoKey:
/*If the command key is pressed, it is a menu selection*/
theKey = (char)(theEvent.message & charCodeMask);
if ( ((theEvent.modifiers & cmdKey) != 0) )
DoMenuSelection(MenuKey(theKey)); /*Our own routine for handling menu selections*/
else
/*Otherwise, it's a normal key down.*/
DoKey(theKey, theEvent.modifiers);
break;
case updateEvt:
/*Find out what event the update event is for.*/
if ( (WindowPtr)theEvent.message == myWindow )
DoUpdate();
break;
case diskEvt:
/*Handle bad disk insertions*/
if (HiWord(theEvent.message) != noErr)
{
DILoad();
DIBadMount(diskInitPt, theEvent.message);
DIUnload();
}
break;
default: ; /*Other events are ignored*/
}; /*case*/
/*For each turn to the event loop, we may to call some "background" process, e.g. animations.*/
DoBackground();
} /*MainLoop*/
/* Standard inits */
static void InitToolbox(void) {
InitGraf (&qd.thePort);
InitFonts ();
FlushEvents (everyEvent,0);
InitWindows ();
InitMenus ();
TEInit ();
InitDialogs (nil);
InitCursor ();
}
/* Main program */
void main(void)
{
InitToolbox();
InitDungeon();
SetupMenus();
NewGame();
/*Initializations done! Run the game loop until the game ends.*/
do
{
MainLoop();
} while (! gDone);
} /*Dungeon*/
/*What's left for making a real game of it?*/
/*- Animations*/
/*- Several levels*/
/*- Faster drawing*/
/*- Asynch sound*/
/*- More objects, i.e. weapons, monsters, treasures…*/